<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Spark • Splat Texture</title>
  <style>
    body {
      margin: 0;
    }
    canvas {
      touch-action: none;
    }
  </style>
</head>

<body>
  <script type="importmap">
    {
      "imports": {
        "three": "/examples/js/vendor/three/build/three.module.js",
        "lil-gui": "/examples/js/vendor/lil-gui/dist/lil-gui.esm.js",
        "@sparkjsdev/spark": "/dist/spark.module.js"
      }
    }
  </script>
  <script type="module">
    import * as THREE from "three";
    import { SparkRenderer, SplatMesh, SparkControls } from "@sparkjsdev/spark";
    import GUI from "lil-gui";
    import { getAssetFileURL } from "/examples/js/get-asset-url.js";

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    window.addEventListener('resize', onWindowResize, false);
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function imgToRgba(img) {
        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);
        const imageData = ctx.getImageData(0, 0, img.width, img.height);
        const rgba = new Uint8Array(imageData.data.buffer);
        return { rgba, width: img.width, height: img.height };
    }

    const textureLoader = new THREE.TextureLoader();
    const starUrl = await getAssetFileURL("star.png");
    const star = imgToRgba((await textureLoader.loadAsync(starUrl)).image);
    const heartUrl = await getAssetFileURL("heart.png");
    const heart = imgToRgba((await textureLoader.loadAsync(heartUrl)).image);

    const splatTexLayers = 32;
    const texData = new Uint8Array(4 * star.width * star.height * splatTexLayers);
    const texData2 = new Uint8Array(4 * star.width * star.height * splatTexLayers);
    for (let z = 0; z < splatTexLayers; z++) {
        const t = z / (splatTexLayers - 1);
        const starOpacity = Math.max((t - 0.5) / 0.5, 0.0);
        const heartOpacity = Math.max((0.5 - t) / 0.5, 0.0);
        const gaussOpacity = Math.max(1 - Math.abs(t - 0.5) / 0.5, 0.0);
        for (let y = 0; y < star.height; y++) {
            for (let x = 0; x < star.width; x++) {
                const inIndex = (y * star.width) + x;
                const outIndex = ((z * star.height) + y) * star.width + x;
                const o4 = outIndex * 4;
                const i4 = inIndex * 4;
                const deltaX = x / star.width - 0.5;
                const deltaY = y / star.height - 0.5;
                const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY) / 0.5;
                const inside = (dist < 1.0) ? 255 : 0;
                let r = t * star.rgba[i4] + (1 - t) * inside;
                let g = t * star.rgba[i4 + 1] + (1 - t) * inside;
                let b = t * star.rgba[i4 + 2] + (1 - t) * inside;
                let a = t * star.rgba[i4 + 3] + (1 - t) * inside;
                texData[o4] = Math.max(0, Math.min(255, Math.round(r)));
                texData[o4 + 1] = Math.max(0, Math.min(255, Math.round(g)));
                texData[o4 + 2] = Math.max(0, Math.min(255, Math.round(b)));
                texData[o4 + 3] = Math.max(0, Math.min(255, Math.round(a)));

                r = starOpacity * star.rgba[i4] + heartOpacity * heart.rgba[i4] + inside * gaussOpacity;
                g = starOpacity * star.rgba[i4 + 1] + heartOpacity * heart.rgba[i4 + 1] + inside * gaussOpacity;
                b = starOpacity * star.rgba[i4 + 2] + heartOpacity * heart.rgba[i4 + 2] + inside * gaussOpacity;
                a = starOpacity * star.rgba[i4 + 3] + heartOpacity * heart.rgba[i4 + 3] + inside * gaussOpacity;
                texData2[o4] = Math.max(0, Math.min(255, Math.round(r)));
                texData2[o4 + 1] = Math.max(0, Math.min(255, Math.round(g)));
                texData2[o4 + 2] = Math.max(0, Math.min(255, Math.round(b)));
                texData2[o4 + 3] = Math.max(0, Math.min(255, Math.round(a)));
            }
        }
    }
    const texture = new THREE.Data3DTexture(texData, star.width, star.height, splatTexLayers);
    texture.needsUpdate = true;
    const texture2 = new THREE.Data3DTexture(texData2, star.width, star.height, splatTexLayers);
    texture2.needsUpdate = true;

    const spark = new SparkRenderer({
        renderer,
        maxStdDev: 1.0,
        focalDistance: 4.0,
    });
    const splatTexture = {
        enable: true,
        texture: texture,
        near: 1.0,
        far: 15,
        mid: 5,
    };
    spark.splatTexture = splatTexture;
    scene.add(spark);

    const gui = new GUI({ title: "DoF settings" });
    const selectedTexture = {
        selection: "texture",
    };
    gui.add(selectedTexture, "selection", ["none", "texture", "texture2"]).name("Splat texture").onChange(() => {
        splatTexture.enable = true;
        if (selectedTexture.selection === "texture") {
            splatTexture.texture = texture;
        } else if (selectedTexture.selection === "texture2") {
            splatTexture.texture = texture2;
        } else {
            splatTexture.enable = false;
        }
    });
    gui.add(spark, "maxStdDev", 0.1, 3, 0.1);
    gui.add(spark, "falloff", 0, 1, 0.01).name("Gaussian falloff");

    const apertureSize = {
        apertureSize: 0.1,
    };
    function updateApertureAngle() {
        splatTexture.mid = spark.focalDistance;
        splatTexture.near = spark.focalDistance / 5.0;
        splatTexture.far = spark.focalDistance * 5.0;
        if (spark.focalDistance > 0) {
            spark.apertureAngle = 2 * Math.atan(0.5 * apertureSize.apertureSize / spark.focalDistance);
        } else {
            spark.apertureAngle = 0.0;
        }
    }
    updateApertureAngle();

    gui.add(spark, "focalDistance", 0, 15, 0.01).name("Focal plane dist")
        .onChange(updateApertureAngle);
    gui.add(apertureSize, "apertureSize", 0, 1, 0.01).name("Aperture size")
        .onChange(updateApertureAngle);

    const splatURL = await getAssetFileURL("valley.spz");
    const background = new SplatMesh({ url: splatURL });
    background.quaternion.set(1, 0, 0, 0);
    background.scale.setScalar(0.5);
    scene.add(background);

    const controls = new SparkControls({ canvas: renderer.domElement });
    renderer.setAnimationLoop(function animate(time) {
      controls.update(camera);
      renderer.render(scene, camera);
    });
  </script>
</body>

</html>
